This page is designed to keep track of our first golf membership at Thornbury Golf Course.

Last round inputted: 2022-04-03.

Scores over time

There are two courses at Thornbury, one full and one par 3. They are separated here. Scores over time are tracked to see if scores are improving (or not!).

Scores for the last 3 (full) rounds played on the big boy course
Ben Doug Lew
73 73 80
107 103 113
101 108 101

#Par 3 course
ggplot(data = fullscores$PAR3scores[!is.na(fullscores$PAR3scores$Score),], aes(x = Date, y = Score, group = Player)) + 
  geom_line(aes(color = Player), size = 1.7) + geom_point(aes(shape = NULL)) + labs(title = "Total Scores for every (short course) round played so far") + scale_x_date(date_breaks = "1 week", date_labels = "%d %b") + theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust=1)) + transition_reveal(Date)

#Summary of Round totals for each player
SumTable<- sapply(unique(fullscores$fullscoresFULL$Player), function(x) summary(subset(fullscores$fullscoresFULL[!is.na(fullscores$fullscoresFULL$Score), "Score"], subset = fullscores$fullscoresFULL$Player == x)), USE.NAMES = T) 
rownames(SumTable)<-  c("Best", "1st Quartile", "Median", "Average", "3rd Quartile", "Worst")

SumTable %>% kableExtra::kbl(caption = "Summary of our rounds as members (full course)", align = "c", escape = FALSE, digits = 0) %>%  kableExtra::kable_classic(full_width = T, html_font = "Cambria")
Summary of our rounds as members (full course)
Ben Doug Lew
Best 73 73 80
1st Quartile 106 104 111
Median 108 108 114
Average 109 106 112
3rd Quartile 115 111 117
Worst 124 115 127
#Summary of Round totals for each player (par 3)
SumTable<- sapply(unique(fullscores$PAR3scores$Player), function(x) summary(subset(fullscores$PAR3scores[!is.na(fullscores$PAR3scores$Score), "Score"], subset = fullscores$PAR3scores$Player == x)), USE.NAMES = T) 
rownames(SumTable)<-  c("Best", "1st Quartile", "Median", "Average", "3rd Quartile", "Worst")

SumTable %>% kableExtra::kbl(caption = "Summary of our rounds as members (Par 3 course)", align = "c", escape = FALSE, digits = 0) %>%  kableExtra::kable_classic(full_width = T, html_font = "Cambria")
Summary of our rounds as members (Par 3 course)
Ben Doug Lew
Best 75 68 75
1st Quartile 83 72 75
Median 86 72 76
Average 85 74 76
3rd Quartile 89 76 77
Worst 93 79 79

Breaking the magic 100 is a intimidating mountain to climb. Doug managed it first at Thornbury on 20/09/21. So far, since we became members on 31/07/2021, there have been a total of 5 sub hundy rounds. Here is the roll of honour:

fullscores$fullscoresFULL[which(fullscores$fullscoresFULL$Score < 100),] %>% kableExtra::kbl(caption = "Sub-Hundy Roll of Honour", align = "c", escape = FALSE, digits = 0, row.names = FALSE) %>%  kableExtra::kable_classic(full_width = T, html_font = "Cambria") %>%  row_spec(1:nrow(fullscores$fullscoresFULL[which(fullscores$fullscoresFULL$Score < 100),]), bold = T, background = "#c9b037")
Sub-Hundy Roll of Honour
Score Player Date
73 Ben 2022-03-12
97 Doug 2021-09-20
99 Doug 2021-10-09
73 Doug 2022-03-12
80 Lew 2022-03-12

Number of rounds played

We want to make sure we use our memberships to the absolute max so it's worth tracking how many rounds we've played.

#+1 par3 round that we lost the scorecard for!

RoundSummary<- rbind(Full = sapply(unique(holescoresFULL$Player), function(x) holescoresFULL %>% filter(Player == x) %>% filter(!duplicated(Date)) %>% na.omit() %>% nrow(), USE.NAMES = T)+1, Par3 = sapply(unique(holescoresPAR3$Player), function(x) holescoresPAR3 %>% filter(Player == x) %>% filter(!duplicated(Date)) %>% na.omit() %>% nrow(), USE.NAMES = T)+1, TotalHoles = sapply(unique(holescoresFULL$Player), function(x) holescoresFULL %>% filter(Player == x) %>% na.omit() %>% nrow(), USE.NAMES = T), MoneySaved = sapply(unique(holescoresFULL$Player), function(x) holescoresFULL %>% filter(Player == x) %>% na.omit() %>% nrow(), USE.NAMES = T)*(34/18) + sapply(unique(holescoresPAR3$Player), function(x) holescoresPAR3 %>% filter(Player == x) %>% na.omit() %>% nrow(), USE.NAMES = T)*(15/18))

RoundSummary %>% kableExtra::kbl(caption = "Times played as members", align = "c", escape = FALSE, digits = 0) %>%  kableExtra::kable_classic(full_width = T, html_font = "Cambria")
Times played as members
Ben Doug Lew
Full 27 30 27
Par3 7 7 5
TotalHoles 423 468 423
MoneySaved 889 974 859

NOTE: The count for the full course includes rounds when 9 holes have been played as well as a full round. One Par3 scorecard was lost to the ether so the scores are not included but that round is still represented in the times played table. Another time on the full course we didn't bother scoring because of the major puddles on the course.

The formula used for working out how much our golf would have cost uses the full price of a round on the full course (£34) and the Par 3 course (£15) and therefore doesn't take into account discount rates at certain times.

\(Cost of one hole = Full price / 18\)

\(Cost of one hole * number of holes played\)

Hole Probing

These box plots show how each player makes it through a round. The black dots are the actual scores and the boxes show the average (blue line through box) and upper/lower quartiles (top/bottom of the box) scores for each hole. From this you may be able to spot your bogey holes (pun intended) or your best scoring holes.

Some numbers to complement these plots:

#get favourite holes by determining best average score (par adjusted?) for each player. maybe do most consistent holes too.
HoleStats<- lapply(unique(Course$`Thornbury Long`$Player), function(x) Course$`Thornbury Long` %>% filter(Player == x) %>% na.omit()) #separate by course + player and get rid of NA rounds
names(HoleStats)<- unique(Course$`Thornbury Long`$Player) #pull names across
  
HoleStatsAdjScore<- lapply(names(HoleStats), function(y) lapply(1:18, function(x) summary(HoleStats[[y]]["Adj_Score"][which(HoleStats[[y]][["Hole"]] == x),]))) #go by adjusted scores for summary stats
names(HoleStatsAdjScore)<- names(HoleStats) #pull names across

HoleStatsAdjScoreSummary<- lapply(HoleStatsAdjScore, unlist) #convert to dataframe for nice table. First unlist then alter dimensions
dim(HoleStatsAdjScoreSummary$Ben)<- c(6,18)
dim(HoleStatsAdjScoreSummary$Doug)<- c(6,18)
dim(HoleStatsAdjScoreSummary$Lew)<- c(6,18)

HoleStatsAdjScoreSummary<- lapply(names(HoleStatsAdjScoreSummary), function(x) t(HoleStatsAdjScoreSummary[[x]][c(1,3,4,6),])) #retain only min, median, mean, max columns
names(HoleStatsAdjScoreSummary)<- names(HoleStats)

HoleStatsAdjScoreSummary<- cbind(HoleStatsAdjScoreSummary$Ben, HoleStatsAdjScoreSummary$Doug, HoleStatsAdjScoreSummary$Lew) #bring all summary player data together

rownames(HoleStatsAdjScoreSummary)<- 1:18 #rename rownames for hole numbers
HoleStatsAdjScoreSummary<- floor(HoleStatsAdjScoreSummary) #convert to integers to make conversion to par etc much easier

HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == -2] <- "eagle"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == -1] <- "birdie"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == 0] <- "par"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == 1] <- "bogey"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == 2] <- "double bogey"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == 3] <- "triple bogey"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == 4] <- "quad bogey"

for(col in 1:ncol(HoleStatsAdjScoreSummary)){
  HoleStatsAdjScoreSummary[,col] <- ifelse(
  HoleStatsAdjScoreSummary[,col] == "birdie",
  cell_spec(HoleStatsAdjScoreSummary[,col], color = "green", bold = T),
  ifelse(HoleStatsAdjScoreSummary[,col] == "par",
  cell_spec(HoleStatsAdjScoreSummary[,col], color = "yellow", bold = T),      
  cell_spec(HoleStatsAdjScoreSummary[,col], color = "black")
))
}

HoleStatsAdjScoreSummary %>% kableExtra::kbl(caption = "Summary by Individual Hole (full course only)", align = "c", escape = FALSE, digits = 0, col.names = rep(c("Best", "Median", "Average", "Worst"), 3), row.names = T) %>% kableExtra::kable_classic(full_width = T, html_font = "Cambria", lightable_options = "striped") %>% kableExtra::add_header_above(header = c("", "Ben" = 4, "Doug" = 4, "Lew" = 4), border_left = TRUE, border_right = TRUE, line = TRUE) %>% column_spec(column = 5:8, background = "slategray1")
Summary by Individual Hole (full course only)
Ben
Doug
Lew
Best Median Average Worst Best Median Average Worst Best Median Average Worst
1 par double bogey double bogey 5 birdie triple bogey double bogey 5 bogey triple bogey triple bogey 5
2 par double bogey double bogey 5 birdie bogey bogey triple bogey par double bogey bogey quad bogey
3 par triple bogey double bogey 6 par double bogey double bogey 5 par double bogey double bogey 5
4 par double bogey double bogey quad bogey par double bogey double bogey 6 par double bogey double bogey 6
5 par double bogey double bogey 6 par double bogey double bogey 6 bogey double bogey double bogey 5
6 birdie bogey bogey 7 par bogey bogey 5 birdie double bogey bogey 5
7 par double bogey double bogey 6 par double bogey bogey quad bogey par double bogey double bogey 5
8 par double bogey double bogey 5 par double bogey double bogey 5 bogey triple bogey double bogey 6
9 bogey double bogey double bogey 6 bogey double bogey double bogey 6 bogey double bogey double bogey quad bogey
10 bogey triple bogey triple bogey 6 bogey double bogey double bogey 5 par triple bogey double bogey 6
11 par bogey bogey triple bogey par bogey bogey triple bogey par bogey bogey quad bogey
12 par bogey bogey quad bogey par bogey bogey quad bogey birdie bogey bogey quad bogey
13 bogey double bogey double bogey quad bogey bogey double bogey double bogey quad bogey par double bogey double bogey 6
14 par bogey bogey triple bogey par bogey bogey quad bogey par bogey bogey 6
15 bogey triple bogey double bogey 5 bogey double bogey double bogey quad bogey par double bogey double bogey 5
16 par bogey bogey 5 par double bogey bogey 5 par double bogey bogey 5
17 bogey double bogey double bogey 6 bogey triple bogey double bogey 6 par triple bogey triple bogey 6
18 par double bogey double bogey 5 par triple bogey double bogey 5 bogey triple bogey triple bogey 5
HoleStatsAdjSD<- lapply(names(HoleStats), function(y) lapply(1:18, function(x) sd(HoleStats[[y]]["Score"][which(HoleStats[[y]][["Hole"]] == x),])))
HoleStatsAdjSD<- lapply(HoleStatsAdjSD, unlist)
names(HoleStatsAdjSD)<- names(HoleStats)

MostConsistent<- lapply(HoleStatsAdjSD, function(x) which(x == min(x)))

LeastConsistent<- lapply(HoleStatsAdjSD, function(x) which(x == max(x)))

Consistency is imperative to a good golf score. Holes that you consistently get the same score on are good markers. However, the consistency may be bad! The most consistent hole(s) for Ben is 14 where he averages bogey but the least consistent is 6 where he averages bogey. The most consistent hole(s) for Doug is 11 where he averages bogey but the least consistent is 1 where he averages double bogey. The most consistent hole(s) for Lew is 2 where he averages bogey but the least consistent is 13 where he averages double bogey.

#get favourite holes by determining best average score (par adjusted?) for each player. maybe do most consistent holes too.
HoleStats<- lapply(unique(Course$`Thornbury Long`$Player), function(x) Course$`Thornbury Short` %>% filter(Player == x) %>% na.omit()) #separate by course + player and get rid of NA rounds
names(HoleStats)<- unique(Course$`Thornbury Long`$Player) #pull names across
  
HoleStatsAdjScore<- lapply(names(HoleStats), function(y) lapply(1:18, function(x) summary(HoleStats[[y]]["Adj_Score"][which(HoleStats[[y]][["Hole"]] == x),]))) #go by adjusted scores for summary stats
names(HoleStatsAdjScore)<- names(HoleStats) #pull names across

HoleStatsAdjScoreSummary<- lapply(HoleStatsAdjScore, unlist) #convert to dataframe for nice table. First unlist then alter dimensions
dim(HoleStatsAdjScoreSummary$Ben)<- c(6,18)
dim(HoleStatsAdjScoreSummary$Doug)<- c(6,18)
dim(HoleStatsAdjScoreSummary$Lew)<- c(6,18)

HoleStatsAdjScoreSummary<- lapply(names(HoleStatsAdjScoreSummary), function(x) t(HoleStatsAdjScoreSummary[[x]][c(1,3,4,6),])) #retain only min, median, mean, max columns
names(HoleStatsAdjScoreSummary)<- names(HoleStats)

HoleStatsAdjScoreSummary<- cbind(HoleStatsAdjScoreSummary$Ben, HoleStatsAdjScoreSummary$Doug, HoleStatsAdjScoreSummary$Lew) #bring all summary player data together

rownames(HoleStatsAdjScoreSummary)<- 1:18 #rename rownames for hole numbers
HoleStatsAdjScoreSummary<- floor(HoleStatsAdjScoreSummary) #convert to integers to make conversion to par etc much easier

HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == -2] <- "eagle"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == -1] <- "birdie"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == 0] <- "par"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == 1] <- "bogey"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == 2] <- "double bogey"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == 3] <- "triple bogey"
HoleStatsAdjScoreSummary[HoleStatsAdjScoreSummary == 4] <- "quad bogey"

for(col in 1:ncol(HoleStatsAdjScoreSummary)){
  HoleStatsAdjScoreSummary[,col] <- ifelse(
  HoleStatsAdjScoreSummary[,col] == "birdie",
  cell_spec(HoleStatsAdjScoreSummary[,col], color = "green", bold = T),
  ifelse(HoleStatsAdjScoreSummary[,col] == "par",
  cell_spec(HoleStatsAdjScoreSummary[,col], color = "yellow", bold = T),      
  cell_spec(HoleStatsAdjScoreSummary[,col], color = "black")
))
}

HoleStatsAdjScoreSummary %>% kableExtra::kbl(caption = "Summary by Individual Hole (Par 3 course only)", align = "c", escape = FALSE, digits = 0, col.names = rep(c("Best", "Median", "Average", "Worst"), 3), row.names = T) %>% kableExtra::kable_classic(full_width = T, html_font = "Cambria", lightable_options = "striped") %>% kableExtra::add_header_above(header = c("", "Ben" = 4, "Doug" = 4, "Lew" = 4), border_left = TRUE, border_right = TRUE, line = TRUE) %>% column_spec(column = 5:8, background = "slategray1")
Summary by Individual Hole (Par 3 course only)
Ben
Doug
Lew
Best Median Average Worst Best Median Average Worst Best Median Average Worst
1 bogey double bogey double bogey triple bogey par double bogey bogey quad bogey bogey bogey bogey triple bogey
2 bogey double bogey double bogey triple bogey par bogey bogey triple bogey double bogey double bogey double bogey double bogey
3 par bogey bogey quad bogey par bogey bogey double bogey bogey bogey bogey bogey
4 par bogey bogey quad bogey par bogey bogey double bogey par double bogey bogey triple bogey
5 bogey double bogey double bogey triple bogey par bogey par bogey bogey bogey bogey bogey
6 birdie bogey par double bogey par par par double bogey par par par bogey
7 par bogey bogey triple bogey birdie par par bogey par bogey bogey double bogey
8 bogey double bogey double bogey 6 bogey bogey bogey triple bogey bogey bogey bogey bogey
9 double bogey triple bogey triple bogey quad bogey bogey bogey bogey triple bogey double bogey double bogey double bogey triple bogey
10 par double bogey bogey triple bogey par bogey bogey double bogey bogey bogey double bogey quad bogey
11 par bogey bogey double bogey par bogey par double bogey par bogey bogey triple bogey
12 par double bogey double bogey 6 par par bogey triple bogey bogey bogey bogey double bogey
13 par par par double bogey par bogey par bogey par par par bogey
14 par bogey bogey double bogey par bogey bogey triple bogey par bogey bogey triple bogey
15 bogey double bogey double bogey triple bogey par bogey bogey triple bogey par bogey bogey double bogey
16 par bogey par double bogey par bogey par double bogey birdie par par double bogey
17 par bogey bogey 5 par bogey bogey double bogey par bogey bogey double bogey
18 birdie double bogey double bogey quad bogey eagle bogey par double bogey bogey bogey bogey bogey
HoleStatsAdjSD<- lapply(names(HoleStats), function(y) lapply(1:18, function(x) sd(HoleStats[[y]]["Score"][which(HoleStats[[y]][["Hole"]] == x),])))
HoleStatsAdjSD<- lapply(HoleStatsAdjSD, unlist)
names(HoleStatsAdjSD)<- names(HoleStats)

MostConsistent<- lapply(HoleStatsAdjSD, function(x) which(x == min(x)))

LeastConsistent<- lapply(HoleStatsAdjSD, function(x) which(x == max(x)))

Front vs Back comparison

I think we all consider Thornbury to be stacked with harder holes at the end of the round. So how do our scores on the front and back 9s compare?

FrontvsBack<- data.frame(na.omit(cbind(Front9 = unlist(lapply(c("Ben", "Doug", "Lew"), function(y) lapply(names(golfscores1$holescoresFULL), function(x) sum(golfscores1$holescoresFULL[[x]][["Score"]][golfscores1$holescoresFULL[[x]][["Player"]] == y & golfscores1$holescoresFULL[[x]][["Hole"]] %in% c(1:9)])))), Back9 = unlist(lapply(c("Ben", "Doug", "Lew"), function(y) lapply(names(golfscores1$holescoresFULL), function(x) sum(golfscores1$holescoresFULL[[x]][["Score"]][golfscores1$holescoresFULL[[x]][["Player"]] == y & golfscores1$holescoresFULL[[x]][["Hole"]] %in% c(10:18)])))), Player = c(rep("Ben", length(unique(golfscores1[["holescoresFULL"]]))), rep("Doug", length(unique(golfscores1[["holescoresFULL"]]))), rep("Lew", length(unique(golfscores1[["holescoresFULL"]])))), Date = as.Date(unlist(lapply(c("Ben", "Doug", "Lew"), function(y) lapply(names(golfscores1$holescoresFULL), function(x) unique(golfscores1$holescoresFULL[[x]][["Date"]][golfscores1$holescoresFULL[[x]][["Player"]] == y])))), origin = "1970-01-01"))))

#I think the na.omit converts things to character so scores need to be returned to numeric
FrontvsBack$Front9<- as.numeric(FrontvsBack$Front9)
FrontvsBack$Back9<- as.numeric(FrontvsBack$Back9)

FrontvsBackTable<- t(format(sapply(unique(FrontvsBack$Player), function(x) summary(subset(FrontvsBack$Front9, subset = FrontvsBack$Player == x)), USE.NAMES = T)[c(1,3,4,6),], digits = 2))

FrontvsBackTable<- cbind(FrontvsBackTable, t(format(sapply(unique(FrontvsBack$Player), function(x) summary(subset(FrontvsBack$Back9, subset = FrontvsBack$Player == x)), USE.NAMES = T)[c(1,3,4,6),], digits = 2)))

FrontvsBackTable %>% kableExtra::kbl(caption = "Front vs Back 9s", align = "c", escape = FALSE, digits = 0, col.names = rep(c("Best", "Median", "Average", "Worst"), 2)) %>% add_header_above(c(" ", "Front 9" = 4, "Back 9" = 4)) %>% column_spec(5, border_right = T) %>%  kableExtra::kable_classic(full_width = T, html_font = "Cambria")
Front vs Back 9s
Front 9
Back 9
Best Median Average Worst Best Median Average Worst
Ben 35 54 54 67 38 54 55 61
Doug 36 53 52 58 37 54 54 60
Lew 40 56 55 65 40 58 57 72

As a group, we average 54 strokes on the front 9 and 55 strokes on the back 9. The best anyone has scored on the front 9 is 35 (the worst is 67). The best anyone has scored on the back 9 is 37 (the worst is 72).

Birdie watch

Tweet tweet, there be birdies around. When were they spotted?

holescores[which(holescores$Adj_Score < 0), c(1,3,6,4,5)] %>% kableExtra::kbl(caption = "Twitchers", align = "c", escape = FALSE, digits = 0, row.names = FALSE) %>%  kableExtra::kable_classic(full_width = T, html_font = "Cambria")
Twitchers
Hole Par Score Date Player
1 5 4 2021-07-31 Doug
18 3 1 2021-09-22 Doug
7 3 2 2021-09-29 Doug
18 3 2 2021-09-29 Ben
6 3 2 2021-10-06 Ben
16 3 2 2021-10-06 Lew
12 3 2 2021-11-14 Lew
6 3 2 2021-11-28 Ben
6 3 2 2021-12-19 Lew
2 3 2 2022-03-12 Doug

Here would be a good time to mention that the round played on the Par 3 course on 22nd September 2021 by the right honourable Douglas is an unverifiable round. Despite us all believing that he "got a hole in one" he did it when noone else was around to see it. The jury will, unfortunately, always be out on this one.

Par performance

Some players may not be able to handle their wood but are deadly with an iron. To roughly gauge long vs short game, Par can be used to see how well our golfing warriors perform on different types of holes. Looking at overall performance on holes with different Par looks a little something like this:

...and as histograms:

Handicap Tracker

These three noble warriors will probably be scratch golfers in no time. So this graph is designed to document their rise to the pro game.

Handicap was calculated by taking the adjusted course score (max quadruple bogey per hole) over the last few rounds then dividing it by the number of rounds included. The official maximum handicap was 28 but it's been upped to 54 now! Par 3 courses have been excluded from the handicap calculations, as have any cheeky 9 hole rounds.

This graph would probably be better with a rolling summary. i.e. for the last 5 rounds record handicap after rounds 1,2,3 then 2,3,4 then 3,4,5. It'll be a better measure of progress over time. Work backwards from the last round using lapply and a sequence. Hard to implement.

Even better would be a proper handicap calculation where the worst and best 4 holes are removed to get a more meaningful gauge of play.